Skip to content
On this page

扩展插件的开发

项目的扩展插件是一个 js 文件, 比如我们测试用的 test.js

javascript
console.log("hello world");

我们在 dist/plugins/config.json 中配置这个扩展插件

json
{
  "plugins": ["test"]
}

然后项目就会自动执行打印了 hello world 了

扩展插件配置说明

参数名类型描述
pluginspluginConfig[]扩展插件列表 按照数组顺序依次执行

pluginConfig 的配置说明如下:

参数名类型描述
nameString扩展插件名称
isAfterLoadBoolean是否等待项目加载后再执行代码
dirBoolean项目是否为目录结构, 如果是目录结构会自动加载目录下的 index.js(name 则是目录名称)

内置插件说明

插件名描述
test测试插件, 提供各种示例代码和插件使用示例
pluginBaseTemplate标准的插件项目工程打包的空模板(测试使用)
loginPlugin官网的登录插件(配合 electron 使用, 用于官网账号登录)
pluginCodeEdit代码编辑插件(可以在线调试代码)
pluginIdentifyPattern图纸识别、符号标注、家装设计、图纸比对

扩展插件项目工程

对于相对复杂的扩展,简单的使用 js 效率特别低,没有很好的开发体验,因为我们基于 vite 创建了一些标准的插件项目模板工程。

项目集成基本说明中,分别提到了三个目录: dist 目录是项目的打包后的代码, MxCAD 目录是扩展插件开发中的示例代码, MxCADiframe 目录相当于你自己的项目集成我们的项目的一个示例工程。

在理解了这三个目录的工程后,我们应该明白, MxCAD 目录就是一个扩展插件项目工程, 但是我们只能去参考它的代码实现,你应该有一个自己的扩展插件。

基于 vite 我们可以很方便的打包到一个扩展插件, 因为扩展插件本质上就是 js 文件。 我们只要把这个 js 文件打包到 dist/plugins 目录下, 然后在 dist/plugins/config.json 中配置这个插件, 就可以生效了。

在开发包中,我们提供了一个标准的扩展工程, 你可以复制它直接开发自己的扩展插件。 位置: MxDrawCloudServer\SRC\sample\Edit\2d\createPluginBaseTemplate

但是为了更理解这个打包工程,下面是这个工程的说明。

一、修改扩展插件的名称 找到 package.json 的 name 属性, 修改成自己的插件名称

二、vite.config.ts 中的 exclude 属性记录了外部依赖,globals 属性记录了外部依赖的全局变量。 这些定义的外部依赖,都可以通过模块化的方式引人。 比如:

ts
import { MxFun } from "mxdraw";

就是从全局变量 Mx 中导出 MxFun 对象

js
const { MxFun } = Mx;

三、请确保扩展工程目录存在一个同级目录是 dist 目录 比如扩展插件目录是: test 那么在 test 目录的同级目录必须存在 dist 目录(我们项目打包后的代码)

四、npm run dev 是开发调试 npm run build 是打包(打包后的 js 文件自动放在 dist/plugins 目录下)

五、MxPluginContext.d.ts 是插件上下文的 ts 类型文件,用于类型提示

插件代码配置

在 mxcad npm 包中 MxCADPluginBase 类, 我们可以通过如下方式修改它的配置,实现在项目初始化时,根据这个配置来初始化项目。

ts
import { MxCADPluginBase, MxCADUI } from "mxcad";
import { MxFun } from "mxdraw";
class MxCADPlugin extends MxCADPluginBase {
  constructor() {
    super();
    // 修改项目初始化默认打开的mxweb文件
    this.openFile = "test3.mxweb";
  }
}
let mxcadUi: MxCADUI;
MxFun.on("mxcadApplicationStart", (mxcadUiImp: MxCADUI) => {
  mxcadUi = mxcadUiImp;
  mxcadUi.init(new MxCADPlugin());
});

具体的 MxCADPluginBase、MxCADUI 的示例提供的配置在 ts 类型提示中有对应说明,请自行查看,一般这些配置都是项目初始化时,可能会用到的配置。

如果你有使用mxcadUi.init(new MxCADPlugin())请确保 scripts/synchronizationProfile.js 中的自动化脚本配置中isAfterLoadfalse。 如果为true,那么插件代码会在项目加载完成后才执行, 导致 MxCADPluginBase 配置不生效`。

插件上下文

在扩展插件中,在 window 对象上暴露了全局变量MxPluginContext

getApp 获取 Vue3 App 实例

这里的 app 就是 vue 的 createApp 创建的 app 实例

ts
import { App } from "vue";
const app = MxPluginContext.getApp();
app.use({
  install(app: App) {
    // vue3 插件
  },
});

实体自定义属性

alt text

上面的展示效果在扩展插件中显示自定义的实体属性实现如下:

ts
import { MxFun } from "mxdraw";
import { MxCpp, McObjectId, MxPropertiesWindowCustomValueType } from "mxcad";
// 等待项目创建mxcad实例
MxFun.on("mxcadApplicationCreatedMxCADObject", (param) => {
  const { mxcad } = param;
  // 获取
  MxCpp.PropertiesWindow.onEvent_getProperties((id: McObjectId) => {
    let ent = id.getMcDbEntity();
    if (!ent) return [];
    let dn = ent.getxDataDouble("DN");
    let len = ent.getxDataDouble("LEN");
    let ret = [];

    if (dn.ret) {
      ret.push({
        sVarName: "DN",
        iVarType: MxPropertiesWindowCustomValueType.kDouble,
        val: dn.val,
        isOnlyRead: false,
      });
    }

    if (len.ret) {
      ret.push({
        sVarName: "LEN",
        iVarType: MxPropertiesWindowCustomValueType.kDouble,
        val: len.val,
        isOnlyRead: false,
      });
    }
    return ret;
  });
  // 设置
  MxCpp.PropertiesWindow.onEvent_setProperties((id: McObjectId, prop: any) => {
    let ent = id.getMcDbEntity();
    if (!ent) return;
    if (prop.sVarName == "DN") {
      ent.setxDataDouble("DN", prop.val);
    } else if (prop.sVarName == "LEN") {
      ent.setxDataDouble("LEN", prop.val);
    }
  });
});

展示自定义属性是有显示的可以通过MxPluginContext.setCustomDataListLength(12)设置最多可以展示的自定义属性数量

store 部分全局数据管理

  1. 命令行输入框自动聚焦 可以通过MxPluginContext.store.useFocus() 控制是否自动聚焦监听输入
ts
const { setCommandFocus, isCommandFocus } = MxPluginContext.useFocus()
setCommandFocus(false) // 关闭命令行输入框用户键盘输入自动聚焦 true 则开启自动聚焦
// 当前是否自动聚焦
console.log(isCommandFocus.value)
// 设置自动聚焦的输入框
setInputEl(document.getElementById("inputEl")

getUiConfig 获取 UI 配置

这是一个动态添加 UI 配置的示例,因为 getUiConfig 获取的配置部分数据是响应式的,你可以直接添加或者修改 UI 配置,并且它可以生效

ts
MxPluginContext.getUiConfig().then((config) => {
  if (!config?.leftDrawerComponents?.includes("CodeEditor"))
    config?.leftDrawerComponents?.push("CodeEditor");
  const uiInfo = {
    icon: "class: iconfont daimabianji",
    prompt: "代码编辑",
    cmd: "Mx_CodeEditor",
  };
  if (!config.mTopButtonBarData) {
    config.mTopButtonBarData = [];
  }
  const isUI = config.mTopButtonBarData.some((info) => {
    return info.cmd === uiInfo.cmd && uiInfo.prompt === uiInfo.prompt;
  });
  if (!isUI) {
    config.mTopButtonBarData.push(uiInfo);
  }
});

添加侧边栏组件

alt text

我们可以自定义实现一个自己的侧边栏,代码示例如下: 我们需要实现三个文件 useComponent.ts

ts
export const isShow = ref(false);

index.vue

vue
<template>
  <div v-show="isShow">
    <v-btn>hello world</v-btn>
  </div>
</template>
<script setup lang="ts">
import { VBtn } from "vuetify/components";
import { isShow } from "./useComponent";
</script>

index.ts

ts
import { defineAsyncComponent } from "vue";
import { isShow } from "./useComponent";

MxPluginContext.addDrawerComponent("Test_Component", {
  isShow: isShow,
  component: defineAsyncComponent(() => import("./index.vue")),
  width: 600,
  title: "hello world",
  cmd: [
    {
      cmd: "Mx_Open_Test_Component",
      call: async () => {
        isShow.value = !isShow.value;
      },
    },
  ],
});
MxPluginContext.getUiConfig().then((config) => {
  if (!config?.leftDrawerComponents?.includes("Test_Component")) {
    config?.leftDrawerComponents?.push("Test_Component");
  }
});

这里有一些关键要素:

  1. UiConfig 的 leftDrawerComponents 或者 rightDrawerComponents 中必须有添加的侧边栏名称, 这里是 Test_Component
  2. 必须实现一个命令来控制侧边栏的打开和关闭逻辑
  3. 组件必须提供 isShow 并只有为 true 才可以显示
  4. 侧边栏组件中可以直接使用 vuetify/components 的组件

vuetify 组件的使用

项目使用了 vuetify 的组件,在扩展插件中一样可以使用组件, 但是 vuetify 的实例在项目中已经挂载,所以我们应该按照如下方式使用:

index.vue

vue
<script setup lang="ts">
import { VBtn, VList, VSelect } from "vuetify/components";
</script>
<template>
  <div>
    <VList>
      <VBtn>hello</VBtn>
      <VSelect :items="[1, 2, 3, 4]"></VSelect>
    </VList>
  </div>
</template>

index.ts

ts
import { createVNode, render } from "vue";
import Index from "./index.vue";
const targetEl = document.querySelector("header div div");
const el = document.createElement("div");

targetEl.appendChild(el);
const vnode = createVNode(Index);
const app = MxPluginContext.getApp();
vnode.appContext = app._context;
render(vnode, el);

这样我们就将组件添加到了 header 头部的 div 元素中。

自定义弹框

我们在上下文中提供了自定义的弹框组件,你可以配合 hooks 按照我们的规范去实现自定义的弹框

属性类型描述
titlestring弹框的标题
logostring左侧的 logo
cardClassstring定义卡片的 css class
cardTextClassstring定义卡片内容区域的 css class
cardActionClassstring定义底部操作区域的 css class
footerBtnList{ name: string, fun?: (...args: any[]) => void, primary?: boolean, labelProps?: { keyName?: string,colon?: boolean,noTextCaption?: boolean}, disabled?: () => boolean }[]底部区域的按钮配置
modelValueboolean显示隐藏
keys{ [key: string]: (e: KeyboardEvent) => any }弹框的快捷键定义
插槽描述
header定义整个弹框的头部
header-icon如果没有 header 插槽,可以定义左侧默认 logo 图片显示区域
default默认插槽, 弹框主体内容
actions底部操作栏

在使用 MxDialog 组件时,必须使用useDialogIsShow这个 hooks 来定义组件组件的一些数据。 下面详细说明useDialogIsShow, 我们可以通过它实现很多使用的效果: dialog.ts

ts
// dialog.isShow 默认是false
// 如果没有传入命令,则只能通过修改dialog.isShow或者调用showDialog来显示弹框
// 如果传入了命令,则调用命令会自动显示或关闭弹框
// 如果传入了第三个参数,则会自定义这个命令的实现
export const dialog = MxPluginContext.useDialogIsShow(
  false,
  "Mx_test_dialog",
  () => {
    dialog.showDialog(!dialog.isShow);
  }
);

dialog.vue

vue
<script setup lang="ts">
import { VBtn, VList, VSelect } from "vuetify/components";
import { dialog } from "./dialog.ts";
const { MxDialog } = MxPluginContext.components;
const footerBtnList = [
  {
    name: "确定",
  },
];
const { isShow } = dialog;
</script>
<template>
  <MxDialog
    title="测试标题"
    v-model="isShow"
    max-width="300"
    :footerBtnList="footerBtnList"
  >
    asdasdasdasdasdasd
    <VBtn
      >asdasdsa
      <template #loader></template>
    </VBtn>
    <input />
    <VBtn>asdasdsa</VBtn>
  </MxDialog>
</template>

index.ts

ts
import dialog from "./dialog.vue";
const el = document.createElement("div");
document.body.appendChild(el);
const vnode = createVNode(dialog);
const app = MxPluginContext.getApp();
vnode.appContext = app._context;
render(vnode, el);

现在我们只需要调用 Mx_test_dialog 命令就可以弹出弹框了, 这是最基本的用法。

我们还可以利用useDialogIsShow实现显示、确认、取消的效果: dialog.ts

ts
// 弹框要显示的数据
type RevealData = { title: string };
// 弹框确认要传入的数据
type ConfirmData = { data: number };
// 弹框取消要传入的数据
type CancelData = { data: number };
export const dialog = MxPluginContext.useDialogIsShow<
  RevealData,
  ConfirmData,
  CancelData
>();

dialog.vue

vue
<script setup lang="ts">
import { ref } from "vue";
import { VBtn, VList, VSelect } from "vuetify/components";
import { dialog } from "./dialog.ts";
const { MxDialog } = MxPluginContext.components;
const footerBtnList = [
  {
    name: "取消",
    fun: () => {
      // 弹框点击了取消按钮,传入数据
      dialog.cancel({ data: 1 });
      // 需要先发送数据再关闭弹框
      dialog.showDialog(false);
    },
  },
  {
    name: "确定",
    fun: () => {
      // 弹框点击了确认按钮,传入数据
      dialog.confirm({ data: 2 });
      dialog.showDialog(false);
    },
    primary: true,
  },
];
const { isShow, onReveal } = dialog;
const title = ref("测试标题");
onReveal((data) => {
  // 接收到显示的消息
  if (data.title) title.value = data.title;
});
</script>
<template>
  <MxDialog
    :title="title"
    v-model="isShow"
    max-width="300"
    :footerBtnList="footerBtnList"
  >
    asdasdasdasdasdasd
    <VBtn
      >asdasdsa
      <template #loader></template>
    </VBtn>
    <input />
    <VBtn>asdasdsa</VBtn>
  </MxDialog>
</template>

index.ts

ts
import DialogComponent from "./dialog.vue";
import { dialog } from "./dialog.ts";
const el = document.createElement("div");
document.body.appendChild(el);
const vnode = createVNode(DialogComponent);
const app = MxPluginContext.getApp();
vnode.appContext = app._context;
render(vnode, el);

// 显示弹框, 并传入显示数据
dialog
  .showDialog(true, { title: "测试标题显示" })
  .then(({ data, isCanceled }) => {
    // 需要等待cancel或者confirm调用才会触发
    if (isCanceled) {
      // 调用dialog.cancel({ data: 1 })取消
      console.log("弹框取消的数据", data.data === 1);
    } else {
      // 调用dialog.confirm({ data: 2 })取消
      console.log("弹框确认的数据", data.data === 2);
    }
  });
// 也可以直接传入显示数据 和 showDialog 的返回结果一致
dialog.reveal({ title: "reveal" }).then(({ data, isCanceled }) => {});

// 我们也可以通过监听确认或者取消事件得到数据
dialog.onConfirm((data) => {}), dialog.onCancel((data) => {});

在弹框打开时,快捷键操作可以这样设置:

vue
<script setup lang="ts">
const { MxDialog } = MxPluginContext.components;
const keys = {
  x: (e) => {
    // 按下x键触发
    console.log(e);
  },
  "ctrl + z": (e) => {
    // 按下Ctrl + z组合键
  },
  escape: (e) => {
    // 按下esc键
  },
};
</script>
<template>
  <MxDialog :keys="keys"> </MxDialog>
</template>

其他

以下具体方法的使用看 ts 类型提示

  1. useTheme 主题相关
  2. useFileName 文件名称显示相关
  3. addCommand 添加命令,通过它添加的命令在命名输入框中会有提示
  4. callCommand 执行命令
  5. useLoading 显示加载框
  6. useMessage 显示消息提示
  7. components 项目中常用的一些组件

还有一些全局的对象,都是第三方库

包名window 挂载的全局对象名称描述
vueVuevue3 框架查看文档
mxcadMxCADmxcad 编辑展示 CAD 图形
mxdrawMx基于three@0.113.2改造的用于 CAD 图形渲染的库查看文档
threeTHREEthree@0.113.2 可以使用适合这个版本的一些示例查看文档
piniapiniavue3 状态管理库 版本@2 查看文档
vuetifyvuetifyvue3 组件库 查看文档
vuetify/componentsvuetifyComponentsvuetify 的所有组件
axiosaxios网络请求的库, 项目使用默认的实例,没有单独创建实例,所以可以对项目中的请求进行全局的拦截和响应,比如设置请求头的需求

国际化 i18n 翻译

已弃用 vue-i18n, 当前实现采用@voerkai18n/cli github 地址

这里简要说明,更多详细说明请参考@voerkai18n/cli 文档

如果你需要国际化 i18n 翻译, 在扩展插件工程中安装依赖:

sh
npm install @voerkai18n/cli -D

然后再 package.json 中添加:

json
{
  "scripts": {
    "i18n": "npm run i18nInit && npm run i18nExtract && npm run i18nAutoTranslate && npm run i18nCompile",
    "i18nInit": "voerkai18n init . -lngs zh en cht -d zh",
    "i18nExtract": "voerkai18n extract -D -f ts,vue",
    "i18nAutoTranslate": "voerkai18n translate --appid xxx --appkey xxx",
    "i18nCompile": "voerkai18n compile -t -l"
  }
}

关于 i18nAutoTranslate 自动翻译

i18nAutoTranslate 自动翻译需要提供百度的 appid 和 appkey 具体如何提供获取 appid 和 appkey 或者需要自定义翻译接口请参考: https://zhangfisher.github.io/voerka-i18n/zh/guide/tools/cli.html#自动翻译-translate

voerkai18n具体命令的参数说明请参考:https://zhangfisher.github.io/voerka-i18n/zh/guide/tools/cli.html#安装

然后我们就可以调用i18nInit命令初始化

sh
npm run i18nInit

初始化会再 src 中生成一个 languages 目录

我们可以通过引入 languages/index.ts 导出 t 函数翻译文本

ts
// 任意ts或者vue文件中
import { t } from "./languages";
const text = t("你好");
console.log(text);

写好后,我们可以通过调用 npm run i18nExtract提取文件中t("")包裹的文字;

然后你可以选择手动翻译在 languages/translates/default.json 中你可以看到如下内容:

json
{
  "你好": {
    "en": "",
    "cht": "",
    "$files": ["src/index.ts"]
  }
}

我们可以再en属性中填写对应的英文名称

当然 default.json 是默认从文件中提取的,有一些没有文字没有办法提取到。 我们可以在 languages/translates 目录下创建一个新的 json 文件, 然后按照上述格式,补充文字和翻译的内容。

如果你设置了voerkai18n translate --appid xxx --appkey xxx中的 appid 和 appkey 或者自定义了翻译接口,

那么, 我们调用npm run i18nAutoTranslate 会自动将 languages/translates 目录中的 json 文件,对应的翻译内容全部通过翻译接口翻译完成。

最后,我们通过npm run i18nCompile 将 json 文件中翻译好的数据,编译成 ts 就实现了国际化 i18n 的功能了, 对于扩展插件工程, 我们应该始终使用使用开发库模式 voerkai18n compile -l也就是要加上-l

为了简化这些步骤,我们可与直接运行npm run i18n 自动提取、翻译、编译, 每次添加了新的t("")函数, 或者 languages/translates 的 json 中修改了数据内容,都可以调用它。

在 vue 中,我们希望切换语音时,vue 组件中的数据可以自动响应式的切换,

我们需要重新定义一个新的t函数来实现它, 注意,必须是 t 函数,因为它是通过t("")正则匹配提取文字的。

i18n.ts

ts
import { ref } from "vue";
import { i18nScope } from "./languages";
const activeLanguage = ref<string>();
i18nScope.on("change", (value) => {
  activeLanguage.value = value;
});
export const t = (message: string, ...args: any[]) => {
  activeLanguage.value;
  return i18nScope.t(message, ...args);
};

我们使用这个 t 函数在组件中使用它,就可以确保在切换语言时,动态的自动切换。

vue
<script setup lang="ts">
import { t } from "./i18n";
</script>
<template>
  <div>{{ t("你好") }}</div>
</template>